package com.sky.migrate.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.sky.migrate.common.DataSourceType;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.binding.BindingException;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.*;
import org.apache.ibatis.type.JdbcType;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

import javax.sql.DataSource;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;

@Intercepts({
        @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class})
})
@Component("prepareInterceptor")
public class PrepareInterceptor implements Interceptor {

    /**
     * 是否开启双写
     */
    @Value("${db-migrate.double-write}")
    private boolean doubleWrite;

    /**
     * 主数据源：oracle/mysql
     */
    @Value("${db-migrate.key}")
    private String key;

    private DataSource mysqlDataSource;

    private DataSource oracleDataSource;

    public PrepareInterceptor(@Qualifier("mysqlDataSource") DataSource mysqlDataSource,
                              @Qualifier("oracleDataSource") DataSource oracleDataSource) {
        this.mysqlDataSource = mysqlDataSource;
        this.oracleDataSource = oracleDataSource;
    }

    /**
     * 日志
     */
    private static final Logger log = LoggerFactory.getLogger(PrepareInterceptor.class);

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // cdl 汇合结果
        CountDownLatch cdl = new CountDownLatch(2);
        log.info("进入 PrepareInterceptor 拦截器...");
        Object[] args = invocation.getArgs();
        MappedStatement ms = (MappedStatement) args[0];
        SqlCommandType sqlCommandType = ms.getSqlCommandType();
        String mapperFullPath = ms.getId();
        AtomicReference<Object> subSourceResult = new AtomicReference<>();
        new Thread(
                () -> {
                    SqlSession sqlSession = null;
                    try {
                        if (doubleWrite) {
                            String mapperPath = mapperFullPath.substring(0, mapperFullPath.lastIndexOf("."));
                            Class<?> mapperClazz = Class.forName(mapperPath);
                            SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
                            sqlSession = sqlSessionFactory.openSession();
                            Object mapper = sqlSession.getMapper(mapperClazz);

                            // 将mybatis封装好的参数转换成原始参数
                            Object parameter = invocation.getArgs()[1];
                            Object[] paramValue = getParamValue(parameter);
                            Method method = getMethod(ms.getId(), mapperClazz);
                            Object[] paramValues = fixNullParam(method, paramValue);

                            int r = 1 / 0;
                            // 反射调用新库 Mapper 的方法,本质上执行的是 MapperProxy.invoke
                            subSourceResult.set(method.invoke(mapper, paramValues));
                            log.info("子线程执行新数据源结果:{}", subSourceResult);
                        }
                    } catch (Exception e) {
                        log.error("子线程执行异常：{}", e.getMessage(), e);
                        subSourceResult.set(e.getMessage());
                    } finally {
                        cdl.countDown();
                        if (sqlSession != null) {
                            sqlSession.close();
                        }
                    }
                }).start();

        Object mainSourceResult = invocation.proceed();
        cdl.countDown();
        cdl.await();
        // 当双写时，对比结果
        compareResult(mainSourceResult, subSourceResult.get(), sqlCommandType, mapperFullPath, args[1]);

        log.info("main线程结果：{}", mainSourceResult);

        // 返回主数据库结果，不影响业务
        return mainSourceResult;
    }

    /**
     * 对比结果并保存
     *
     * @param mainSourceResult 主数据源结果
     * @param subSourceResult  新数据源结果
     * @param sqlCommandType   sql类型：insert/update/delete/select
     * @param arg              入参
     */
    @Async
    protected void compareResult(Object mainSourceResult, Object subSourceResult, SqlCommandType sqlCommandType, String path, Object arg) {
        if (!doubleWrite) {
            return;
        }
        try {
            // 将对象转成json
            ObjectMapper mapper = new ObjectMapper();
            String mainStr = mapper.writeValueAsString(mainSourceResult);
            String subStr = mapper.writeValueAsString(subSourceResult);
            if (!mainStr.equals(subStr)) {
                // 对比不一致：记录错误结果
                log.info("operate:{},路径：{},req-str:{},main-result:{},sub-result:{}", sqlCommandType, path, arg, mainStr, subStr);
                // todo 记录监控数据库，实际中，监控的表是不一样的，需要调用外部接口，这里简单日志记录
            }
        } catch (Exception e) {
            log.error("双写对比异常:{}", e.getMessage(), e);
        }
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
    }

    private SqlSessionFactory getSqlSessionFactory() throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(mysqlDataSource);
        Configuration configuration = new Configuration();
        configuration.setMapUnderscoreToCamelCase(true);
        configuration.setUseGeneratedKeys(false);
        configuration.setJdbcTypeForNull(JdbcType.NULL);
        // 设置成新数据源
        if (DataSourceType.Type.ORACLE.equals(DataSourceType.Type.valueOf(StringUtils.upperCase(key)))) {
            configuration.setDatabaseId(DataSourceType.Type.MYSQL.getName());
            sqlSessionFactoryBean.setDataSource(mysqlDataSource);
        } else {
            configuration.setDatabaseId(DataSourceType.Type.ORACLE.getName());
            sqlSessionFactoryBean.setDataSource(oracleDataSource);
        }
        sqlSessionFactoryBean.setConfiguration(configuration);
        sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mybatis/mapper/*.xml"));
        return sqlSessionFactoryBean.getObject();
    }

    private Object[] getParamValue(Object parameter) {
        List<Object> paramValues = new ArrayList<>();

        if (parameter instanceof Map) {
            Map<String, Object> paramMap = (Map<String, Object>) parameter;
            if (paramMap.containsKey("collection")) {
                paramValues.add(paramMap.get("collection"));
            } else if (paramMap.containsKey("array")) {
                paramValues.add(paramMap.get("array"));
            } else {
                int count = 1;
                while (count <= paramMap.size() / 2) {
                    try {
                        paramValues.add(paramMap.get("param" + (count++)));
                    } catch (BindingException e) {
                        break;
                    }
                }
            }
        } else if (parameter != null) {
            paramValues.add(parameter);
        }
        return paramValues.toArray();
    }

    private Object[] fixNullParam(Method method, Object[] paramValues) {
        if (method.getParameterTypes().length > 0 && paramValues.length == 0) {
            return new Object[]{null};
        }
        return paramValues;
    }

    private Method getMethod(String id, Class mapperClass) throws NoSuchMethodException {
        //获取参数对应的 class
        String methodName = id.substring(id.lastIndexOf(".") + 1);
        String key = id;

        Method method = null;
        if (method == null) {
            method = findMethodByMethodSignature(mapperClass, methodName);
            if (method == null) {
                throw new NoSuchMethodException("No such method " + methodName + " in class " + mapperClass.getName());
            }
        }
        return method;
    }

    private Method findMethodByMethodSignature(Class mapperClass, String methodName) throws NoSuchMethodException {
        // mybatis 的 Mapper 内的方法不支持重载，所以这里只要方法名匹配到了就行，不用进行参数的匹配
        Method method = null;
        for (Method m : mapperClass.getMethods()) {
            if (m.getName().equals(methodName)) {
                method = m;
                break;
            }
        }
        return method;
    }
}
